Skip to main content

TradingCost

TradingCost describes the fees and slippage that apply when an order fills. It can be attached per symbol on a TradingStrategy, or set as a market-level default on the PortfolioConfiguration. Both backtest engines apply it to fill prices and trade values; in live mode, the broker reports actual costs.

from investing_algorithm_framework import TradingCost

Signature

TradingCost(
symbol: str | None = None,
fee_percentage: float = 0.0,
slippage_percentage: float = 0.0,
fee_fixed: float = 0.0,
slippage_model: SlippageModel | None = None,
)
ParameterTypeDefaultDescription
symbolstr | NoneNoneTarget symbol (e.g. "BTC"). None means "market default" when used at the portfolio level. Symbol matching is case-insensitive.
fee_percentagefloat0.0Variable fee in percent of trade value (e.g. 0.1 = 0.1 %).
slippage_percentagefloat0.0Slippage in percent of price. Buys fill higher, sells fill lower. Ignored when slippage_model is set.
fee_fixedfloat0.0Flat fee per trade in the trading currency, added on top of fee_percentage.
slippage_modelSlippageModel | NoneNonePluggable slippage model. When set, overrides slippage_percentage. See Slippage Models below.

How Costs Are Applied

For each fill the engine computes:

buy_fill_price  = price * (1 + slippage_percentage / 100)
sell_fill_price = price * (1 - slippage_percentage / 100)

fee = trade_value * fee_percentage / 100 + fee_fixed

When a slippage_model is set, the model's calculate_slippage() method replaces the percentage formula above. The fee calculation stays the same.

trade_value is computed at the slippage-adjusted price, so fees compound on top of slippage — matching how real exchanges quote post-trade cost.

Resolution Order

When the engine needs a TradingCost for a symbol it walks this fallback chain:

  1. Strategy-level — first matching TradingCost in TradingStrategy.trading_costs whose symbol matches.
  2. Portfolio defaultsfee_percentage / slippage_percentage on PortfolioConfiguration (or app.add_market(...)), used when the strategy doesn't override the symbol.
  3. Zero cost — singleton fallback so every code path always gets a TradingCost.

This means market-level defaults set on the portfolio quietly apply to every symbol unless a strategy explicitly overrides them.

Examples

Per-symbol fees and slippage

class MyStrategy(TradingStrategy):
symbols = ["BTC", "ETH"]
trading_costs = [
TradingCost(
symbol="BTC",
fee_percentage=0.10,
slippage_percentage=0.05,
),
TradingCost(
symbol="ETH",
fee_percentage=0.10,
slippage_percentage=0.10, # ETH less liquid here
),
]

Realistic broker model

trading_costs = [
TradingCost(
symbol="BTC",
fee_percentage=0.06, # 6 bps maker/taker blend
fee_fixed=0.50, # flat per-order ticket
slippage_percentage=0.02,
),
]

Stress-testing a strategy

Bump fees to see how robust your edge is:

trading_costs = [
TradingCost(symbol="BTC", fee_percentage=0.5), # 50 bps
TradingCost(symbol="ETH", fee_percentage=0.5),
]

If your strategy still has positive expectancy at 50 bps round-trip, real-world fee variance is unlikely to kill it.

Market-level defaults

Set defaults once on the portfolio, override per symbol on the strategy:

PortfolioConfiguration(
market="BITVAVO",
initial_balance=10_000,
trading_symbol="EUR",
fee_percentage=0.10, # default for every symbol on this market
slippage_percentage=0.05,
)

Interaction With Other Rules

  • PositionSize — costs are applied to the order produced from the size; the size itself is not pre-deducted.
  • StopLossRule / TakeProfitRule — slippage is applied to the exit fill (sell direction), so reported PnL already reflects realistic exits.
  • ScalingRule — every scale-in and scale-out is a separate fill and pays fees independently.

Slippage Models

The slippage_model parameter lets you plug in sophisticated slippage behavior that goes beyond a flat percentage. When set, it overrides the slippage_percentage field.

from investing_algorithm_framework import (
TradingCost,
VolumeShareSlippage,
FixedSlippage,
FixedBasisPointsSlippage,
)

VolumeShareSlippage

Models slippage as a function of the order's share of bar volume with a quadratic price impact. Also enforces a volume limit — at most volume_limit fraction of a bar's volume can be filled per bar. Orders exceeding this limit are partially filled and re-evaluated on subsequent bars.

TradingCost(
symbol="BTC",
fee_percentage=0.1,
slippage_model=VolumeShareSlippage(
volume_limit=0.025, # max 2.5% of bar volume
price_impact=0.1, # price impact coefficient
),
)
ParameterDefaultDescription
volume_limit0.025Max fraction of bar volume that can fill per bar (0.025 = 2.5 %).
price_impact0.1Coefficient for quadratic impact: impact = price_impact × (amount / volume)².

Impact formula:

participation = amount / volume
impact = price_impact * participation²

buy_fill_price = price * (1 + impact)
sell_fill_price = price * (1 - impact)

This is the most realistic built-in model — strategies that trade illiquid assets or large positions relative to volume will see significant market impact, and orders larger than the volume limit will be partially filled.

FixedSlippage

Adds or subtracts a fixed amount from the order price. Useful for markets with a known, relatively stable spread.

TradingCost(
symbol="ETH",
fee_percentage=0.1,
slippage_model=FixedSlippage(amount=0.50), # fixed $0.50 spread
)
ParameterDefaultDescription
amount0.01Fixed slippage in price units.

FixedBasisPointsSlippage

Slippage expressed in basis points (1 bp = 0.01 % of price). Convenient when you want a proportional slippage without thinking in decimals.

TradingCost(
symbol="BTC",
fee_percentage=0.1,
slippage_model=FixedBasisPointsSlippage(basis_points=5), # 5 bps = 0.05%
)
ParameterDefaultDescription
basis_points5Slippage in basis points.

Custom Slippage Model

Create your own by extending SlippageModel:

from investing_algorithm_framework import SlippageModel

class MySlippageModel(SlippageModel):
def __init__(self, price_impact=0.1, volume_limit=0.025):
self.price_impact = price_impact
self.volume_limit = volume_limit

def calculate_slippage(self, price, order_side, amount=None, volume=None):
"""Return adjusted fill price."""
if amount and volume and volume > 0:
impact = self.price_impact * (amount / volume) ** 2
else:
impact = 0.0

if order_side == "BUY":
return price * (1 + impact)
return price * (1 - impact)

def max_fill_amount(self, order_amount, volume=None):
"""Return maximum fillable amount for this bar."""
if volume and volume > 0:
return min(order_amount, volume * self.volume_limit)
return order_amount

The two methods you can override:

MethodRequiredDescription
calculate_slippage(price, order_side, amount, volume)YesReturn the adjusted fill price after slippage.
max_fill_amount(order_amount, volume)NoReturn the maximum fillable amount per bar. Default returns the full order_amount (no volume limit).

Choosing a Slippage Approach

ApproachWhen to use
slippage_percentageQuick approximation, don't need volume awareness.
FixedSlippageKnown fixed spread (e.g. a specific venue).
FixedBasisPointsSlippageProportional slippage in familiar units (basis points).
VolumeShareSlippageRealistic simulation — large orders impact price, fills are volume-limited.
Custom SlippageModelAny other behavior (e.g. time-of-day effects, asymmetric slippage).
Backward Compatibility

Setting slippage_model is fully optional. Existing strategies using slippage_percentage continue to work unchanged.

See Also